'''Useful comparison functions for sorting bibliographies.

These comparison functions, like the built-in function C{cmp}, take two
arguments and return a negative number if the first comes before the
second and a positive number if the second comes before the first. They
may be used as an argument to the built in sort function or to the
Bibliography.sort() instance method.

@copyright: (c) 2010, Benjamin Kalish

@license:
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful, but
    WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    General Public License for more details.
'''

from bkn.bibtex.string_tools import *
from bkn.bibtex.names import *
from bkn.bibtex.error import BKNBibtexError
from bkn.bibtex.utilities import ensure_objects_are_unicode

def compare_names(a,b):
    '''Compares two strings representing names.
    
    @param a: a name
    @type a: C{Unicode}
    @param b: a name
    @type b: C{Unicode}
    @rtype: C{int}
    @return:
        a negative integer if C{a} should come first, a positive integer
        if C{b} should come first, and 0 if C{a} and C{b} are equal.
    @todo:
        create a Name object and modify this method to work with Name
        objects as well as strings.
    @todo:
        add error handling in case invalid arguments are passed
    @todo:
        document method of comparison
    @todo:
        This method is long! Consider refactoring!
    '''
    ensure_objects_are_unicode(a,b)
    (a_first, a_von, a_last, a_jr) = first_von_last_jr(a)
    (b_first, b_von, b_last, b_jr) = first_von_last_jr(b)
    a_last = (a_von + ' ' + a_last).lower()
    b_last = (b_von + ' ' + b_last).lower()
    
    if a_last != b_last:
        # compare last names level 1: ignore spaces, punctuation and accents
        a_test = re.sub('\W+', '', tex2ascii(a_last))
        b_test = re.sub('\W+', '', tex2ascii(b_last))
        if a_test != b_test:
            return cmp(a_test, b_test)
        # compare last names level 2: ignore accents but take spaces and
        # punctuation into account
        a_test = tex2ascii(a_last)
        b_test = tex2ascii(b_last)
        if a_test != b_test:
            return cmp(a_test, b_test)
        # compare last names level #: take accents into account
        # we do this according to a cheap hack which sets the order
        # to the natural sorting of latex control sequences. If accents
        # become prevalent we may want to switch to something better.
        return cmp(a_last, b_last)
    
    a_first = a_first.lower()
    b_first = b_first.lower()
    if a_first != b_first:
        # compare first names level 1: ignore accents
        a_test = tex2ascii(a_first)
        b_test = tex2ascii(b_first)
        # compare first names level #: take accents into account
        # we do this according to a cheap hack which sets the order
        # to the natural sorting of latex control sequences. If accents
        # become prevalent we may want to switch to something better.
        return cmp(a_first, b_first)
    
    a_jr = a_jr.lower()
    b_jr = b_jr.lower()
    return cmp(a_jr, b_jr)
    
def compare_name_strings(a,b):
    '''Compare two strings, which may be lists of authors, as found in BibTex.
    
    @param a: a list of authors' names as found in BibTeX
    @type a: C{Unicode}
    @param b: a list of authors' names as found in BibTeX
    @type b: C{Unicode}
    @rtype: C{int}
    @return:
        a negative integer if C{a} should come first, a positive integer
        if C{b} should come first, and 0 if C{a} and C{b} are equal.
    @todo:
        This or a similar method should take either a list of strings or
        a list of Name objects.
    '''
    ensure_objects_are_unicode(a,b)
    a_authors = author_ls(a)
    b_authors = author_ls(b)
    limit = min(len(a_authors), len(b_authors))
    for i in range(0, limit):
        test = compare_names(a_authors[i], b_authors[i])
        if test != 0:
            return test
    return cmp(len(a_authors), len(b_authors))
    
def compare_year_strings(a,b):
    '''Compare two strings each of which represent a year.
    
    This function is mildly tolerant of irregularities in the specified
    years.
    
    @param a: a string containing a four-digit year
    @type a: C{Unicode}
    @param b: a string containing a four-digit year
    @type b: C{Unicode}
    @rtype: C{int}
    @return:
        a negative integer if C{a} should come first, a positive integer
        if C{b} should come first, and 0 if C{a} and C{b} are equal.
    @note:
        This method assumes that the year is given by first sequence of
        four digits found in the string. Ties will be broken by
        comparing the entire string. And a string with no such sequence
        will come after a string with such a sequence.
    '''
    ensure_objects_are_unicode(a,b)
    test = cmp(_year_for_sort(a),_year_for_sort(b))
    if test != 0:
        return test
    return cmp(a,b)
    
    
def _year_for_sort(year_string):
    '''Returns the first sequence of four digits found in the string,
    or a very large integer if no such sequence is found.
    
    @param year_string: a string containing a four-digit year
    @type year_string: C{Unicode}
    @rtype: C{int}
    @return:
        the integer value of the first four consecutive digits found in
        C{year_string}
    '''
    ensure_objects_are_unicode(year_string)
    mo = re.search(r'\d+', year_string)
    try:
        return int(mo.group(0))
    except:
        return sys.maxint

def compare_titles(a,b):
    '''Compares titles, ignoring case, punctuation, and accents.
   
    This method ignores capitalization and the initial articles "the",
    "a", and "an".
    
    @param a: a title
    @type a: C{Unicode}
    @param b: a title
    @type b: C{Unicode}
    @rtype: C{int}
    @return:
        a negative integer if C{a} should come first, a positive integer
        if C{b} should come first, and 0 if C{a} and C{b} are equal.
    '''
    ensure_objects_are_unicode(a,b)
    a_normalized = re.sub('\W+', ' ', a).lower()
    a_normalized = re.sub(r'^(the |a |an )', '', a_normalized, re.IGNORECASE)
    b_normalized = re.sub('\W+', ' ', b).lower()
    b_normalized = re.sub(r'^(the |a |an )', '', b_normalized, re.IGNORECASE)
    return cmp(a_normalized, b_normalized)


def compare_entries(a,b):
    '''A standard comparison used for sorting bibliographies.
    
    Sorts by year, then author (or editor) and finally by title.
    
    @param a: a bibliography entry
    @type a: L{Record}
    @param b: a bibliography entry
    @type b: L{Record}
    @rtype: C{int}
    @return:
        a negative integer if C{a} should come first, a positive integer
        if C{b} should come first, and 0 if C{a} and C{b} are equivalent
        for sorting purposes.
    '''
    test = compare_year_strings(a[u'year'],b[u'year'])
    if test != 0:
       return test
    
    # then sort by author or editor field
    test = compare_name_strings(a.names_for_sort(), b.names_for_sort())
    if test != 0:
        return test
    
    # then sort by title
    test = compare_titles(a[u'title'],b[u'title'])
    if test != 0:
        return test
    
    # if we still haven't found a difference, give up
    return 0
